Skip to content

fix: support complete agent card URL in A2ACardResolver#837

Open
ehsavoie wants to merge 1 commit into
a2aproject:mainfrom
ehsavoie:issue_771
Open

fix: support complete agent card URL in A2ACardResolver#837
ehsavoie wants to merge 1 commit into
a2aproject:mainfrom
ehsavoie:issue_771

Conversation

@ehsavoie
Copy link
Copy Markdown
Collaborator

URI.resolve() silently drops intermediate path segments when the
resolved path starts with '/', causing incorrect URLs for agents
hosted under a path prefix (e.g. /spec03). Replace with direct
concatenation and add a pass-through guard when the supplied URL
already ends with the agent card path.

Fixes #771 🦕

@ehsavoie ehsavoie requested a review from jmesnil April 29, 2026 09:59
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request enhances A2ACardResolver to accept either a base URL or a complete agent card URL, updating the internal URL construction logic to handle these variations correctly. It also introduces several unit tests to validate URL resolution with tenants, custom paths, and authentication headers, while improving error handling in the test suite. Feedback was provided to address a potential double-slash issue when concatenating the base URL and the agent card path.

Comment thread http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java Outdated
@ehsavoie
Copy link
Copy Markdown
Collaborator Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the A2ACardResolver to support base URLs that already include the agent card path, ensuring the resolver uses such URLs verbatim. The URL construction logic was modified to handle manual concatenation when URI.resolve() is insufficient, and comprehensive unit tests were added to cover new functionality and error cases. Feedback was provided regarding a potential double-slash issue in the manual URL concatenation logic, along with a suggestion for a more robust normalization approach.

Comment thread http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java Outdated
Copy link
Copy Markdown
Contributor

@jmesnil jmesnil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should make the card resolver more complex.
I see 2 use case for fetching an agent card:

  • use the spec's well-known endpoint that is appended to whatever the baseUrl is (it can be http://example.com, http://example.com/agents/v1/foo)
  • point to a specific URL that returns an agent card. This does not have to end with .well-known/.agent-card.json. This URL can be fetched from a registry for example. In that case, there is no URL resolution at all, it's used verbatim.

So the API would be:

  • A2AResolver(String url) (and all its variant)
  • AgentCard getWellKnownAgentCard() -> append `.well-known/.agent-card.json to the base URL
  • AgentCard getAgentCard() -> use the URL verbatim

That's be a API break change so maybe we could have just a new method:

  • AgentCard getAgentCard(boolean fromWellKnownLocation)
    • getAgentCard() delegates to getAgentCard(true)
    • the resolution of the URL is deferred until getAgentCard(boolean) is called to figure out which URL to connect to.

@ehsavoie
Copy link
Copy Markdown
Collaborator Author

ehsavoie commented May 4, 2026

@jmesnil I broke the API because I think it is cleaner and clearer that way

@kabir
Copy link
Copy Markdown
Collaborator

kabir commented May 18, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the A2ACardResolver utility to use a builder pattern and introduces separate methods for retrieving well-known and configured agent cards. It also adds logging, improves URL resolution logic, and updates relevant tests and examples. Feedback was provided regarding the authHeader builder method, which could throw an UnsupportedOperationException if an immutable map is used, and a suggestion was made to normalize the Authorization header casing for consistency.

@ehsavoie
Copy link
Copy Markdown
Collaborator Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the A2ACardResolver to use a builder pattern and introduces distinct methods for fetching well-known versus configured agent cards. It includes URL normalization logic to handle trailing slashes and path prefixes more robustly, along with enhanced logging and updated test coverage. Feedback was provided regarding the resolveAgentCardUrl method, noting that its path-matching guard is currently hardcoded to the default path and should instead dynamically check against the specific path being resolved to avoid incorrect results when custom paths are used.

Comment thread http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java Outdated
@ehsavoie ehsavoie force-pushed the issue_771 branch 2 times, most recently from e915a3b to 103105c Compare May 19, 2026 15:03
Copy link
Copy Markdown
Collaborator

@kabir kabir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also since I think this changes the API the prefix to the PR title should be "fix!" rather than "fix"

* @deprecated Use {@link #getConfiguredAgentCard()} for fetching the configured card URL, or
* {@link #getWellKnownAgentCard()} for the standard well-known endpoint.
*/
@Deprecated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just not keep this method and remove the two new methods?
It seems a bit cumbersome to have the user choose between getConfiguredAgentCard() and getWellKnownAgentCard().
In my mind, if one is configured we return that, and if not we fallback to the well known one

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always have both since both are based on the builtBaseUri

Comment thread http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java Outdated
Comment thread http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java Outdated
Comment thread http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java Outdated
  URI.resolve() silently drops intermediate path segments when the
  resolved path starts with '/', causing incorrect URLs for agents
  hosted under a path prefix (e.g. /spec03). Replace with direct
  concatenation and add a pass-through guard when the supplied URL
  already ends with the agent card path.

Signed-off-by: Emmanuel Hugonnet <ehugonne@redhat.com>
@ehsavoie
Copy link
Copy Markdown
Collaborator Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors A2ACardResolver to use a builder pattern and introduces separate methods for fetching well-known and configured agent cards, updating all client usages and tests accordingly. The review feedback points out a critical breaking change from removing public constructors and a bug where configuredAgentCardUrl incorrectly defaults to the base URL instead of the well-known URL when the card path is empty. Additional suggestions include allowing authHeaders to be explicitly cleared in the builder and simplifying the resolver logic in A2A.java once the default URL bug is resolved.

Comment on lines +96 to 112
private A2ACardResolver(A2AHttpClient httpClient, String baseUrl, @Nullable String tenant, @Nullable String agentCardPath, @Nullable Map<String, String> authHeaders) throws A2AClientError {
checkNotNullParam("httpClient", httpClient);
checkNotNullParam("baseUrl", baseUrl);
this.httpClient = httpClient;
try {
URI builtBaseUri = new URI(org.a2aproject.sdk.util.Utils.buildBaseUrl(new AgentInterface("JSONRPC", baseUrl, ""), tenant));
this.wellKnownUrl = resolveAgentCardUrl(builtBaseUri, DEFAULT_AGENT_CARD_PATH).toString();
this.configuredAgentCardUrl = (agentCardPath == null || agentCardPath.isBlank())
? builtBaseUri.toString()
: resolveAgentCardUrl(builtBaseUri, agentCardPath).toString();

LOGGER.debug("Initialized A2ACardResolver with wellKnownUrl={}, configuredAgentCardUrl={}", wellKnownUrl, configuredAgentCardUrl);
} catch (URISyntaxException e) {
throw new A2AClientError("Invalid agent URL", e);
}
this.authHeaders = authHeaders;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Removing all public constructors from A2ACardResolver is a breaking change for any external clients/consumers of this SDK. To maintain backward compatibility, we should keep the most common constructors and mark them as @Deprecated instead of removing them entirely.

Additionally, configuredAgentCardUrl should default to wellKnownUrl when agentCardPath is null or blank. Currently, it defaults to builtBaseUri.toString(), which points to the base URL of the agent (e.g., http://localhost:9999) rather than the agent card URL (e.g., http://localhost:9999/.well-known/agent-card.json). This breaks the deprecated getAgentCard() method and any direct usage of getConfiguredAgentCard() where agentCardPath is not explicitly set.

    @Deprecated
    public A2ACardResolver(String baseUrl) throws A2AClientError {
        this(A2AHttpClientFactory.create(), baseUrl, null, null, null);
    }

    @Deprecated
    public A2ACardResolver(String baseUrl, @Nullable String tenant) throws A2AClientError {
        this(A2AHttpClientFactory.create(), baseUrl, tenant, null, null);
    }

    private A2ACardResolver(A2AHttpClient httpClient, String baseUrl, @Nullable String tenant, @Nullable String agentCardPath, @Nullable Map<String, String> authHeaders) throws A2AClientError {
        checkNotNullParam("httpClient", httpClient);
        checkNotNullParam("baseUrl", baseUrl);
        this.httpClient = httpClient;
        try {
            URI builtBaseUri = new URI(org.a2aproject.sdk.util.Utils.buildBaseUrl(new AgentInterface("JSONRPC", baseUrl, ""), tenant));
            this.wellKnownUrl = resolveAgentCardUrl(builtBaseUri, DEFAULT_AGENT_CARD_PATH).toString();
            this.configuredAgentCardUrl = (agentCardPath == null || agentCardPath.isBlank())
                    ? this.wellKnownUrl
                    : resolveAgentCardUrl(builtBaseUri, agentCardPath).toString();

            LOGGER.debug("Initialized A2ACardResolver with wellKnownUrl={}, configuredAgentCardUrl={}", wellKnownUrl, configuredAgentCardUrl);
        } catch (URISyntaxException e) {
            throw new A2AClientError("Invalid agent URL", e);
        }
        this.authHeaders = authHeaders;
    }

Copy link
Copy Markdown
Collaborator Author

@ehsavoie ehsavoie May 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree, we could have the final url passed as the base url without any agentPath

Comment on lines +187 to +192
public Builder authHeaders(@Nullable Map<String, String> authHeaders) {
if (authHeaders != null) {
this.authHeaders = new HashMap<>(authHeaders);
}
return this;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If authHeaders is null, the builder currently leaves this.authHeaders unmodified. To allow clearing headers or setting them to null explicitly, we should assign null when the parameter is null.

        public Builder authHeaders(@Nullable Map<String, String> authHeaders) {
            this.authHeaders = authHeaders != null ? new HashMap<>(authHeaders) : null;
            return this;
        }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want null headers

Comment on lines +402 to +404
return (relativeCardPath == null || relativeCardPath.isBlank())
? resolver.getWellKnownAgentCard()
: resolver.getConfiguredAgentCard();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

With the fix applied to A2ACardResolver where configuredAgentCardUrl correctly defaults to wellKnownUrl when agentCardPath is null or blank, we can simplify this logic and remove the ternary operator check.

        return resolver.getConfiguredAgentCard();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: spec.AgentCard does not allow many agents on same domain

3 participants